Completed
Push — master ( 627b48...9c7df1 )
by Olivier
46:55 queued 43:16
created

SlideShow._isMainlyDark   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 64
rs 8.6346
c 1
b 0
f 0
cc 5
nc 6
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
/**
2
 * Nextcloud - Gallery
3
 *
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Olivier Paroz <[email protected]>
9
 *
10
 * @copyright Olivier Paroz 2017
11
 */
12
/* global Gallery, Thumbnails, DOMPurify */
13
(function ($, OC, OCA, t) {
14
	"use strict";
15
	/**
16
	 * Slideshow featuring zooming
17
	 *
18
	 * @constructor
19
	 */
20
	var SlideShow = function () {
21
	};
22
23
	SlideShow.prototype = {
24
		slideshowTemplate: null,
25
		container: null,
26
		zoomablePreviewContainer: null,
27
		controls: null,
28
		imageCache: {},
29
		/** {Image} */
30
		currentImage: null,
31
		errorLoadingImage: false,
32
		onStop: null,
33
		zoomablePreview: null,
34
		active: false,
35
		backgroundToggle: false,
36
		// We need 6 hexas for comparison reasons
37
		darkBackgroundColour: '#000000',
38
		lightBackgroundColour: '#ffffff',
39
40
		/**
41
		 * Initialises the slideshow
42
		 *
43
		 * @param {boolean} autoPlay
44
		 * @param {number} interval
45
		 * @param {Array} features
46
		 */
47
		init: function (autoPlay, interval, features) {
48
			if (features.indexOf('background_colour_toggle') > -1) {
49
				this.backgroundToggle = true;
50
			}
51
52
			return $.when(this._getSlideshowTemplate()).then(function ($tmpl) {
53
				// Move the slideshow outside the content so we can hide the content
54
				$('body').append($tmpl);
55
				this.container = $('#slideshow');
56
				this.zoomablePreviewContainer = this.container.find('.bigshotContainer');
57
				this.zoomablePreview = new SlideShow.ZoomablePreview(this.container);
58
				this.controls =
59
					new SlideShow.Controls(
60
						this,
61
						this.container,
62
						this.zoomablePreview,
63
						interval,
64
						features);
65
				this.controls.init();
66
67
				this._initControlsAutoFader();
68
69
				// Only modern browsers can manipulate history
70
				if (history && history.pushState) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable history is declared in the current environment, consider using typeof history === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
71
					// Stop the slideshow when backing out.
72
					$(window).bind('popstate.slideshow', function () {
73
						if (this.active === true) {
74
							this.active = false;
75
							this.controls.stop();
76
						}
77
					}.bind(this));
78
				}
79
			}.bind(this)).fail(function () {
80
				OC.Notification.show(t('core', 'Error loading slideshow template'));
81
			});
82
		},
83
84
		/**
85
		 * Refreshes the slideshow's data
86
		 *
87
		 * @param {{name:string, url: string, path: string, fallBack: string}[]} images
88
		 * @param {boolean} autoPlay
89
		 */
90
		setImages: function (images, autoPlay) {
91
			this._hideImage();
92
			this.images = images;
93
			this.controls.update(images, autoPlay);
94
		},
95
96
		/**
97
		 * Launches the slideshow
98
		 *
99
		 * @param {number} index
100
		 *
101
		 * @returns {*}
102
		 */
103
		show: function (index) {
104
			this.hideErrorNotification();
105
			this.active = true;
106
			this.container.show();
107
			this.container.css('background-position', 'center');
108
			this._hideImage();
109
			this.container.find('.icon-loading-dark').show();
110
			var currentImageId = index;
111
			return this.loadImage(this.images[index]).then(function (img) {
112
				this.container.css('background-position', '-10000px 0');
113
114
				// check if we moved along while we were loading
115
				if (currentImageId === index) {
116
					var image = this.images[index];
117
					var transparent = this._isTransparent(image.mimeType);
118
					this.controls.showActionButtons(transparent, Gallery.token, image.permissions);
119
					this.errorLoadingImage = false;
120
					this.currentImage = img;
121
					img.setAttribute('alt', image.name);
122
					$(img).css('position', 'absolute');
123
					$(img).css('background-color', image.backgroundColour);
124
					if (transparent && this.backgroundToggle === true) {
125
						var $border = 30 / window.devicePixelRatio;
126
						$(img).css('outline', $border + 'px solid ' + image.backgroundColour);
127
					}
128
129
					this.zoomablePreview.startBigshot(img, this.currentImage, image.mimeType);
130
131
					this._setUrl(image.path);
132
					this.controls.show(currentImageId);
133
					this.container.find('.icon-loading-dark').hide();
134
				}
135
			}.bind(this), function () {
136
				// Don't do anything if the user has moved along while we were loading as it would
137
				// mess up the index
138
				if (currentImageId === index) {
139
					this.errorLoadingImage = true;
140
					this.showErrorNotification(null);
141
					this._setUrl(this.images[index].path);
142
					this.images.splice(index, 1);
143
					this.controls.updateControls(this.images, this.errorLoadingImage);
144
				}
145
			}.bind(this));
146
		},
147
148
		/**
149
		 * Loads the image to show in the slideshow and preloads the next one
150
		 *
151
		 * @param {Object} preview
152
		 *
153
		 * @returns {*}
154
		 */
155
		loadImage: function (preview) {
156
			var url = preview.url;
157
			var mimeType = preview.mimeType;
158
159
			if (!this.imageCache[url]) {
160
				this.imageCache[url] = new $.Deferred();
161
				var image = new Image();
0 ignored issues
show
Bug introduced by
The variable Image seems to be never declared. If this is a global, consider adding a /** global: Image */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
162
163
				image.onload = function () {
164
					preview.backgroundColour = this._getBackgroundColour(image, mimeType);
165
					if (this.imageCache[url]) {
166
						this.imageCache[url].resolve(image);
167
					}
168
				}.bind(this);
169
				image.onerror = function () {
170
					if (this.imageCache[url]) {
171
						this.imageCache[url].reject(url);
172
					}
173
				}.bind(this);
174
				if (mimeType === 'image/svg+xml') {
175
					image.src = this._getSVG(url);
176
				} else {
177
					image.src = url;
178
				}
179
			}
180
			return this.imageCache[url];
181
		},
182
183
		/**
184
		 * Shows a new image in the slideshow and preloads the next in the list
185
		 *
186
		 * @param {number} current
187
		 * @param {Object} next
188
		 */
189
		next: function (current, next) {
190
			this.show(current).then(function () {
191
				// Preloads the next image in the list
192
				this.loadImage(next);
193
			}.bind(this));
194
		},
195
196
		/**
197
		 * Determines which colour to use for the background
198
		 *
199
		 * @param {*} image
200
		 * @param {string} mimeType
201
		 *
202
		 * @returns {string}
203
		 * @private
204
		 */
205
		_getBackgroundColour: function (image, mimeType) {
206
			var backgroundColour = this.darkBackgroundColour;
207
			if (this._isTransparent(mimeType) && this._isMainlyDark(image)) {
208
				backgroundColour = this.lightBackgroundColour;
209
			}
210
			return backgroundColour;
211
		},
212
213
		/**
214
		 * Calculates the luminance of an image to determine if an image is mainly dark
215
		 *
216
		 * @param {*} image
217
		 *
218
		 * @returns {boolean}
219
		 * @private
220
		 */
221
		_isMainlyDark: function (image) {
222
			var isMainlyDark = false;
223
			var numberOfSamples = 1000; // Seems to be the sweet spot
224
			// The name has to be 'canvas'
225
			var lumiCanvas = document.createElement('canvas');
226
227
			var imgArea = image.width * image.height;
228
			var canArea = numberOfSamples;
229
			var factor = Math.sqrt(canArea / imgArea);
230
231
			var scaledWidth = factor * image.width;
232
			var scaledHeight = factor * image.height;
233
			lumiCanvas.width = scaledWidth;
234
			lumiCanvas.height = scaledHeight;
235
			var lumiCtx = lumiCanvas.getContext('2d');
236
			lumiCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
237
			var imgData = lumiCtx.getImageData(0, 0, lumiCanvas.width, lumiCanvas.height);
238
			var pix = imgData.data; // pix.length will be approximately 4*numberOfSamples (for RGBA)
239
			var pixelArraySize = pix.length;
240
			var totalLuminance = 0;
241
			var sampleNumber = 1;
242
			var averageLuminance;
243
			var totalAlpha = 0;
244
			var alphaLevel;
245
			var red = 0;
246
			var green = 0;
247
			var blue = 0;
248
			var alpha = 0;
249
			var lum = 0;
250
			var alphaThreshold = 0.1;
251
252
			var sampleCounter = 0;
253
			var itemsPerPixel = 4; // red, green, blue, alpha
254
			// i += 4 because 4 colours for every pixel
255
			for (var i = 0, n = pixelArraySize; i < n; i += itemsPerPixel) {
256
				sampleCounter++;
257
				alpha = pix[i + 3] / 255;
258
				totalAlpha += alpha;
259
				if (Math.ceil(alpha * 100) / 100 > alphaThreshold) {
260
					red = pix[i];
261
					green = pix[i + 1];
262
					blue = pix[i + 2];
263
					// Luminance formula from
264
					// http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
265
					lum = (red + red + green + green + green + blue) / 6;
266
					//lum = (red * 0.299 + green * 0.587 + blue * 0.114 );
267
					totalLuminance += lum * alpha;
268
					sampleNumber++;
269
				}
270
			}
271
272
			// Deletes the canvas
273
			lumiCanvas = null;
0 ignored issues
show
Unused Code introduced by
The assignment to lumiCanvas seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
274
275
			// Calculate the optimum background colour for this image
276
			averageLuminance = Math.ceil((totalLuminance / sampleNumber) * 100) / 100;
277
			alphaLevel = Math.ceil((totalAlpha / numberOfSamples) * 100);
278
279
			if (averageLuminance < 60 && alphaLevel < 90) {
280
				isMainlyDark = true;
281
			}
282
283
			return isMainlyDark;
284
		},
285
286
		/**
287
		 * Stops the slideshow
288
		 */
289
		stop: function () {
290
			this.active = false;
291
			this.images = null;
292
			this._hideImage();
293
			if (this.onStop) {
294
				this.onStop();
295
			}
296
		},
297
298
		/**
299
		 * Sends the current image as a download
300
		 *
301
		 * @param {string} downloadUrl
302
		 *
303
		 * @returns {boolean}
304
		 */
305
		getImageDownload: function (downloadUrl) {
306
			OC.redirect(downloadUrl);
307
			return false;
308
		},
309
310
		/**
311
		 * Changes the colour of the background of the image
312
		 */
313
		toggleBackground: function () {
314
			var toHex = function (x) {
315
				return ("0" + parseInt(x).toString(16)).slice(-2);
316
			};
317
			var container = this.zoomablePreviewContainer.children('img');
318
			var rgb = container.css('background-color').match(/\d+/g);
319
			var hex = "#" + toHex(rgb[0]) + toHex(rgb[1]) + toHex(rgb[2]);
320
			var $border = 30 / window.devicePixelRatio;
321
			var newBackgroundColor;
322
323
			// Grey #363636
324
			if (hex === this.darkBackgroundColour) {
325
				newBackgroundColor = this.lightBackgroundColour;
326
			} else {
327
				newBackgroundColor = this.darkBackgroundColour;
328
			}
329
330
			container.css('background-color', newBackgroundColor);
331
			if (this.backgroundToggle === true) {
332
				container.css('outline', $border + 'px solid ' + newBackgroundColor);
333
			}
334
		},
335
336
		/**
337
		 * Shows an error notification
338
		 *
339
		 * @param {string} message
340
		 */
341
		showErrorNotification: function (message) {
342
			if ($.isEmptyObject(message)) {
343
				message = t('gallery',
344
					'<strong>Error!</strong> Could not generate a preview of this file.<br>' +
345
					'Please go to the next slide while we remove this image from the slideshow');
346
			}
347
			this.container.find('.notification').html(message);
348
			this.container.find('.notification').show();
349
			this.controls.hideButton('.changeBackground');
350
		},
351
352
		/**
353
		 * Hides the error notification
354
		 */
355
		hideErrorNotification: function () {
356
			this.container.find('.notification').hide();
357
			this.container.find('.notification').html('');
358
		},
359
360
		/**
361
		 * Removes a specific button from the interface
362
		 *
363
		 * @param button
364
		 */
365
		removeButton: function (button) {
366
			this.controls.removeButton(button);
367
		},
368
369
		/**
370
		 * Deletes an image from the slideshow
371
		 *
372
		 * @param {object} image
373
		 * @param {number} currentIndex
374
		 */
375
		deleteImage: function (image, currentIndex) {
376
			// These are Gallery specific commands to be replaced
377
			// which should sit somewhere else
378
			if (!window.galleryFileAction) {
379
				delete Gallery.imageMap[image.path];
380
				delete Thumbnails.map[image.file];
381
				Gallery.albumMap[Gallery.currentAlbum].images.splice(currentIndex, 1);
382
				Gallery.view.init(Gallery.currentAlbum);
383
			}
384
		},
385
386
		/**
387
		 * Automatically fades the controls after 3 seconds
388
		 *
389
		 * @private
390
		 */
391
		_initControlsAutoFader: function () {
392
			var inactiveCallback = function () {
393
				this.container.addClass('inactive');
394
			}.bind(this);
395
			var inactiveTimeout = setTimeout(inactiveCallback, 3000);
396
397
			this.container.on('mousemove touchstart', function () {
398
				this.container.removeClass('inactive');
399
				clearTimeout(inactiveTimeout);
400
				inactiveTimeout = setTimeout(inactiveCallback, 3000);
401
			}.bind(this));
402
		},
403
404
		/**
405
		 * Simplest way to detect if image is transparent.
406
		 *
407
		 * That's very inaccurate since it doesn't include images which support transparency
408
		 *
409
		 * @param mimeType
410
		 * @returns {boolean}
411
		 * @private
412
		 */
413
		_isTransparent: function (mimeType) {
414
			return !(mimeType === 'image/jpeg'
415
				|| mimeType === 'image/x-dcraw'
416
				|| mimeType === 'application/font-sfnt'
417
				|| mimeType === 'application/x-font'
418
			);
419
		},
420
421
		/**
422
		 * Changes the browser Url, based on the current image
423
		 *
424
		 * @param {string} path
425
		 * @private
426
		 */
427
		_setUrl: function (path) {
428
			if (history && history.replaceState) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable history is declared in the current environment, consider using typeof history === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
429
				history.replaceState('', '', '#' + encodeURI(path));
430
			}
431
		},
432
433
		/**
434
		 * Hides the current image (before loading the next)
435
		 *
436
		 * @private
437
		 */
438
		_hideImage: function () {
439
			this.zoomablePreviewContainer.empty();
440
			this.controls.hideActionButtons();
441
		},
442
443
		/**
444
		 * Retrieves an SVG
445
		 *
446
		 * An SVG can't be simply attached to a src attribute like a bitmap image
447
		 *
448
		 * @param {string} source
449
		 *
450
		 * @returns {*}
451
		 * @private
452
		 */
453
		_getSVG: function (source) {
454
			var svgPreview = null;
455
			// DOMPurify only works with IE10+ and we load SVGs in the IMG tag
456
			if (window.btoa &&
457
				document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image",
458
					"1.1")) {
459
				var xmlHttp = new XMLHttpRequest();
0 ignored issues
show
Bug introduced by
The variable XMLHttpRequest seems to be never declared. If this is a global, consider adding a /** global: XMLHttpRequest */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
460
				xmlHttp.open("GET", source, false);
461
				xmlHttp.send(null);
462
				if (xmlHttp.status === 200) {
463
					var pureSvg = DOMPurify.sanitize(xmlHttp.responseText, {ADD_TAGS: ['filter']});
464
					// Remove XML comment garbage left in the purified data
465
					var badTag = pureSvg.indexOf(']&gt;');
466
					var fixedPureSvg = pureSvg.substring(badTag < 0 ? 0 : 5, pureSvg.length);
467
					svgPreview = "data:image/svg+xml;base64," + window.btoa(fixedPureSvg);
468
				}
469
			}
470
471
			return svgPreview;
472
		},
473
474
		/**
475
		 * Retrieves the slideshow's template
476
		 *
477
		 * @returns {*}
478
		 * @private
479
		 */
480
		_getSlideshowTemplate: function () {
481
			var defer = $.Deferred();
482
			if (!this.$slideshowTemplate) {
483
				var self = this;
484
				var url = OC.generateUrl('apps/gallery/slideshow', null);
485
				$.get(url, function (tmpl) {
486
						var template = $(tmpl);
487
						var tmplButton;
488
						var buttonsArray = [
489
							{
490
								el: '.next',
491
								trans: t('gallery', 'Next')
492
							},
493
							{
494
								el: '.play',
495
								trans: t('gallery', 'Play')
496
							},
497
							{
498
								el: '.pause',
499
								trans: t('gallery', 'Pause')
500
							},
501
							{
502
								el: '.previous',
503
								trans: t('gallery', 'Previous')
504
							},
505
							{
506
								el: '.exit',
507
								trans: t('gallery', 'Close')
508
							},
509
							{
510
								el: '.downloadImage',
511
								trans: t('gallery', 'Download'),
512
								toolTip: true
513
							},
514
							{
515
								el: '.changeBackground',
516
								trans: t('gallery', 'Toggle background'),
517
								toolTip: true
518
							},
519
							{
520
								el: '.deleteImage',
521
								trans: t('gallery', 'Delete'),
522
								toolTip: true
523
							}
524
						];
525
						for (var i = 0; i < buttonsArray.length; i++) {
526
							var button = buttonsArray[i];
527
528
							tmplButton = template.find(button.el);
529
							tmplButton.val(button.trans);
530
							if (button.toolTip) {
531
								tmplButton.attr("title", button.trans);
532
							}
533
						}
534
						self.$slideshowTemplate = template;
535
						defer.resolve(self.$slideshowTemplate);
536
					})
537
					.fail(function () {
538
						defer.reject();
539
					});
540
			} else {
541
				defer.resolve(this.$slideshowTemplate);
542
			}
543
			return defer.promise();
544
		}
545
	};
546
547
	window.SlideShow = SlideShow;
548
})(jQuery, OC, OCA, t);
549